/*
 * @(#)FastGradientPainter.java 12/12/2004
 *
 * Copyright 2002 - 2005 JIDE Software Inc. All rights reserved.
 */
package com.element.color;

import com.element.ui.svg.icon.fill.SwordSvg;
import com.element.util.SwingTestUtil;
import net.miginfocom.swing.MigLayout;

import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.Arrays;

/**
 * 该类通过缓存渐变色的矩形图像，并在需要的时候裁剪图像实现形状的渐变，虽然快速，但是裁剪带来的锯齿问题无法解决
 */
public class FastGradientPainter {
	private static final GradientCache gradientCache = new GradientCache();

	//no instantiation
	private FastGradientPainter() {
	}

	/**
	 * Clears the gradient cache
	 */
	public static void clearGradientCache() {
		gradientCache.clear();
	}

	/**
	 * Draws a rectangular gradient in a vertical or horizontal direction. The drawing operations are hardware optimized
	 * whenever possible using the Java2D hardware rendering facilities. The result is gradient rendering approaching
	 * the performance of flat color rendering.
	 *
	 * @param g2         Graphics2D instance to use for rendering
	 * @param s          shape confines of gradient
	 * @param startColor starting color for gradient
	 * @param endColor   ending color fro gradient
	 * @param isVertical specifies a vertical or horizontal gradient
	 */
	public static void drawGradient(Graphics2D g2, Shape s, Color startColor, Color endColor, boolean isVertical) {
		Rectangle r = s.getBounds();
		if (r.height <= 0 || r.width <= 0) return;

		int length = isVertical ? r.height : r.width;
		GradientInfo info = new GradientInfo(g2.getDeviceConfiguration(), length, startColor, endColor, isVertical);

		BufferedImage gradient = gradientCache.retrieve(info);
		if (gradient == null) {
			gradient = createGradientTile(info);
			gradientCache.store(info, gradient);
		}

		// 先裁剪再填充渐变，会有锯齿问题
		Shape prevClip = null;
		boolean nonRectangular = false;
		if (!r.equals(s)) {
			Object oldHints = g2.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
			g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
			nonRectangular = true;
			prevClip = g2.getClip();
			g2.clip(s);
			g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldHints);
		}

		if (isVertical) {
			int w = gradient.getWidth();
			int loops = r.width / w;
			for (int i = 0; i < loops; i++)
				g2.drawImage(gradient, r.x + i * w, r.y, null);
			int rem = r.width % w;
			if (rem > 0) {
				g2.drawImage(gradient, r.x + loops * w, r.y, r.x + loops * w + rem, r.y + length, 0, 0, rem, length, null);
			}
		} else {
			int h = gradient.getHeight();
			int loops = r.height / h;
			for (int i = 0; i < loops; i++)
				g2.drawImage(gradient, r.x, r.y + i * h, null);
			int rem = r.height % h;
			if (rem > 0) {
				g2.drawImage(gradient, r.x, r.y + loops * h, r.x + length, r.y + loops * h + rem, 0, 0, length, rem, null);
			}
		}
		if (nonRectangular) {
			g2.setClip(prevClip);
		}
	}

	public static void main(String[] args) {
		EventQueue.invokeLater(() -> {
			JPanel p = SwingTestUtil.init(new MigLayout());
			Shape shape = SwordSvg.of(50, 50).getShape();

			p.add(new JLabel() {
				@Override
				protected void paintComponent(Graphics g) {
					drawGradient((Graphics2D) g, shape, ColorUtil.PRIMARY, ColorUtil.WARNING, false);
				}

				@Override
				public Dimension getPreferredSize() {
					return new Dimension(200, 200);
				}
			});

			SwingTestUtil.test();
		});
	}

	private static BufferedImage createGradientTile(GradientInfo info) {
		boolean t = info.startColor.getTransparency() > 1 || info.endColor.getTransparency() > 1;

		int dx, dy, w, h;
		if (info.isVertical) {
			dx = 0;
			h = dy = info.length;
			w = 32;
		} else {
			w = dx = info.length;
			dy = 0;
			h = 32;
		}
		BufferedImage img = info.gfxConfig.createCompatibleImage(w, h, t ? Transparency.TRANSLUCENT : Transparency.OPAQUE);
		Paint gp = new GradientPaint(0, 0, info.startColor, dx, dy, info.endColor);

		Graphics2D g = img.createGraphics();
		g.setPaint(gp);
		g.fillRect(0, 0, w, h);
		g.dispose();
		return img;
	}
}

/**
 * Contains all information pertaining to a particular gradient.
 */
class GradientInfo {
	GraphicsConfiguration gfxConfig;
	int length;
	Color startColor, endColor;
	boolean isVertical;

	public GradientInfo(GraphicsConfiguration gc, int ln, Color sc, Color ec, boolean v) {
		gfxConfig = gc;
		length = ln;
		startColor = sc;
		endColor = ec;
		isVertical = v;
	}

	boolean isEquivalent(GradientInfo gi) {
		return (gi.gfxConfig.equals(gfxConfig) && gi.length == length && gi.startColor.equals(startColor) && gi.endColor.equals(endColor) && gi.isVertical == isVertical);
	}

	@Override
	public boolean equals(Object o) {
		if (!(o instanceof GradientInfo)) return false;
		return isEquivalent((GradientInfo) o);
	}

	@Override
	public String toString() {
		return "Direction:" + (isVertical ? "ver" : "hor") + ", Length: " + length + ", Color1: " + Integer.toString(startColor.getRGB(), 16) + ", Color2: " + Integer.toString(endColor.getRGB(), 16);
	}
}

class GradientCacheEntry extends SoftReference {
	GradientCacheEntry next;
	BufferedImage gradient;
	int length;

	GradientCacheEntry(GradientInfo info, BufferedImage gradient, ReferenceQueue queue, GradientCacheEntry next) {
		super(info, queue);
		this.next = next;
		this.gradient = gradient;
		length = info.length;
	}

	GradientInfo getInfo() {
		return (GradientInfo) get();
	}

	BufferedImage getGradient() {
		return gradient;
	}
}

/**
 * 使用 SoftReferences 的缓存，用于内存高效处理渐变。
 * <p>
 * 这个类有些年头了，有机会使用现有的List进行重构
 */
class GradientCache {
	private GradientCacheEntry[] gradients;
	private int size;
	private int threshold;
	private final float loadFactor;
	private final ReferenceQueue queue = new ReferenceQueue();

	GradientCache() {
		this.loadFactor = 0.75f;
		threshold = 16;
		gradients = new GradientCacheEntry[16];
	}

	BufferedImage retrieve(GradientInfo info) {
		int ln = info.length;
		GradientCacheEntry[] grads = getGradients();
		int index = bucket(ln, grads.length);
		GradientCacheEntry e = grads[index];

		while (e != null) {
			GradientInfo egi = e.getInfo();
			try {
				if (egi != null) {
					if (e.length == ln && egi.isEquivalent(info)) {
						return e.gradient;
					}
				}
			} catch (NullPointerException npe) {
				// apparently egi will or e will be cleared anyways sometimes,
				// so we have to catch a possible NPE
				// I print the values to get a better understanding of the situation.
				// comment this if unacceptable or change to use logging if needed
//                System.err.println("e = " + e);
//                System.err.println("egi = " + egi);
			}
			e = e.next;
		}
		return null;
	}

	Object store(GradientInfo info, BufferedImage gradient) {
		GradientCacheEntry[] grads = getGradients();
		int i = bucket(info.length, grads.length);

		GradientCacheEntry e = grads[i];

		if (!entryNotInCache(e, info)) {
			System.err.println("Duplicate entry found!");
		}

		grads[i] = new GradientCacheEntry(info, gradient, queue, e);
		if (++size >= threshold)
			resize(grads.length << 1);
		return null;
	}

	void clear() {
		GradientCacheEntry[] a = getGradients();
		Arrays.fill(a, null);
		size = 0;
		threshold = 16;
		gradients = new GradientCacheEntry[16];
	}

	private boolean entryNotInCache(GradientCacheEntry e, GradientInfo info) {
		while (e != null && e.getInfo() != null) { // to fix a NPE
			if (e.length == info.length && e.getInfo().isEquivalent(info)) {
				return false;
			}
			e = e.next;
		}
		return true;
	}

	private void resize(int newCapacity) {
		GradientCacheEntry[] oldArray = getGradients();
		int oldCapacity = oldArray.length;
		if (oldCapacity == ((Integer.MAX_VALUE >> 1) + 1)) {
			threshold = Integer.MAX_VALUE;
			return;
		}

		GradientCacheEntry[] newArray = new GradientCacheEntry[newCapacity];
		moveEntries(oldArray, newArray);
		gradients = newArray;

		if (size >= (threshold >> 1)) {
			threshold = (int) (newCapacity * loadFactor);
		} else {
			cleanOldCacheEntries();
			moveEntries(newArray, oldArray);
			gradients = oldArray;
		}
	}

	private GradientCacheEntry[] getGradients() {
		cleanOldCacheEntries();
		return gradients;
	}

	private static int bucket(int h, int length) {
		return h & (length - 1);
	}

	private void moveEntries(GradientCacheEntry[] src, GradientCacheEntry[] dest) {
		for (int j = 0; j < src.length; ++j) {
			GradientCacheEntry e = src[j];
			src[j] = null;
			while (e != null) {
				GradientCacheEntry next = e.next;
				Object o = e.get();
				if (o == null) {
					e.next = null;
					e.gradient = null;
					size--;
				} else {
					int i = bucket(e.length, dest.length);
					e.next = dest[i];
					dest[i] = e;
				}
				e = next;
			}
		}
	}

	private void cleanOldCacheEntries() {
		GradientCacheEntry e;
		while ((e = (GradientCacheEntry) queue.poll()) != null) {
			int i = bucket(e.length, gradients.length);

			GradientCacheEntry prev = gradients[i];
			GradientCacheEntry p = prev;
			while (p != null) {
				GradientCacheEntry next = p.next;
				if (p == e) {
					if (prev == e) gradients[i] = next;
					else prev.next = next;
					e.next = null;
					e.gradient = null;
					size--;
					break;
				}
				prev = p;
				p = next;
			}
		}
	}
}
